package top.wugy.blog.searcher;

import com.jfinal.kit.PathKit;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.Page;
import com.jfinal.plugin.activerecord.Record;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.NIOFSDirectory;
import org.lionsoul.jcseg.analyzer.JcsegAnalyzer;
import org.lionsoul.jcseg.tokenizer.core.JcsegTaskConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.wugy.blog.kits.BlogKit;
import top.wugy.blog.model.TBlog;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * wugy on 2017/11/15 10:11
 */
public class LuceneSearcher implements ISearcher {

    private static Analyzer analyzer;
    private static Directory directory;
    private static Lock lock = new ReentrantLock();

    private static final Logger logger = LoggerFactory.getLogger(LuceneSearcher.class);

    public LuceneSearcher() {
        try {
            directory = NIOFSDirectory.open(Paths.get(PathKit.getWebRootPath() + "/luceneIndexes"));
            analyzer = new JcsegAnalyzer(JcsegTaskConfig.COMPLEX_MODE);//new IKAnalyzer();
        } catch (IOException e) {
            logger.error("init lucene path error:" + e);
            e.printStackTrace();
        }
    }

    private IndexWriter getWriter() throws Exception {
        boolean _lock = lock.tryLock(300, TimeUnit.SECONDS);
        while (!_lock)
            _lock = lock.tryLock(300, TimeUnit.SECONDS);
        return new IndexWriter(directory, new IndexWriterConfig(analyzer));
    }

    @Override
    public Page<Record> search(int pageNum, int pageSize, String keyword) {
        try (IndexReader indexReader = DirectoryReader.open(directory)) {
            IndexSearcher indexSearcher = new IndexSearcher(indexReader);
            Query query = query(keyword);

            Sort sort = new Sort(new SortField("sortTime", SortField.Type.LONG, true));
            ScoreDoc scoreDoc = null;
            if (pageNum != 1) {
                int num = pageSize * (pageNum - 1);
                scoreDoc = indexSearcher.search(query, num, sort).scoreDocs[num - 1];
            }
            TopDocs topDocs = indexSearcher.searchAfter(scoreDoc, query, pageSize, sort);

            List<Record> recordList = getRecordList(query, indexSearcher, topDocs);
            int totalRow = searchTotalRecord(indexSearcher, query);
            int totalPages = totalRow % pageSize == 0 ? totalRow / pageSize : totalRow / pageSize + 1;
            return new Page<>(recordList, pageNum, pageSize, totalPages, totalRow);
        } catch (Exception e) {
            logger.error("搜索失败:" + e);
            e.printStackTrace();
        }
        return new Page<>();
    }

    @Override
    public void reloadIndex() {
        try (IndexWriter writer = getWriter()) {
            writer.deleteAll();
            List<SearcherBean> recordList = TBlog.dao.findAll(false);
            if (BlogKit.notEmpty(recordList)) {
                List<Document> documents = BlogKit.newArrayList(recordList.size());
                recordList.forEach(bean -> documents.add(createDoc(bean)));
                writer.addDocuments(documents);
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("reload lucene index error:" + e);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void addRecord(SearcherBean bean) {
        try (IndexWriter writer = getWriter()) {
            writer.addDocument(createDoc(bean));
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("add record to lucene error:" + e);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void updateRecord(SearcherBean bean) {
        deleteRecord(bean.getId() + "");
        addRecord(bean);
    }

    private void deleteRecord(String recordId) {
        try (IndexWriter writer = getWriter()) {
            writer.deleteDocuments(new Term("id", recordId));
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("delete record from lucene error, recordId = " + recordId, e);
        } finally {
            lock.unlock();
        }
    }

    private int searchTotalRecord(IndexSearcher indexSearcher, Query query) throws IOException {
        TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
        return null == topDocs || null == topDocs.scoreDocs ? 0 : topDocs.scoreDocs.length;
    }

    private List<Record> getRecordList(Query query, IndexSearcher indexSearcher, TopDocs topDocs) throws Exception {
        List<Record> recordList = BlogKit.newArrayList();
        for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
            Document doc = indexSearcher.doc(scoreDoc.doc);
            recordList.add(new Record().set("id", doc.get("id")).set("createTime", highlighter(query, doc, "createTime"))
                    .set("tag", highlighter(query, doc, "tag")).set("title", highlighter(query, doc, "title"))
                    .set("content", highlighter(query, doc, "content")));
        }
        return recordList;
    }

    private String highlighter(Query query, Document doc, String field) throws Exception {
        Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("<font color=\"red\">", "</font>"),
                new QueryScorer(query));
        String fieldValue = doc.get(field);
        String highlighterStr = highlighter.getBestFragment(analyzer, field, fieldValue);
        return StrKit.notBlank(highlighterStr) ? highlighterStr : fieldValue;
    }

    private Query query(String keyword) throws Exception {
        return new BooleanQuery.Builder().setMinimumNumberShouldMatch(1)
                .add(new BooleanClause(new QueryParser("title", analyzer).parse(keyword), BooleanClause.Occur.SHOULD))
                .add(new BooleanClause(new QueryParser("content", analyzer).parse(keyword), BooleanClause.Occur.SHOULD))
                .add(new BooleanClause(new QueryParser("tag", analyzer).parse(keyword), BooleanClause.Occur.SHOULD))
                .add(new BooleanClause(new QueryParser("createTime", analyzer).parse(keyword), BooleanClause.Occur.SHOULD))
                .build();
    }

    private Document createDoc(SearcherBean bean) {
        return new DocumentBuilder().add(new StringField("id", bean.getId() + "", Field.Store.YES))
                .add(new TextField("title", bean.getTitle(), Field.Store.YES))
                .add(new TextField("content", bean.getContent(), Field.Store.YES))
                .add(new TextField("tag", bean.getTag(), Field.Store.YES))
                .add(new TextField("createTime", bean.getCreateTime(), Field.Store.YES))
                .add(new NumericDocValuesField("sortTime", BlogKit.toUnix(bean.getCreateTime())))
                .build();
    }

    private static final class DocumentBuilder {

        private static Document document;

        DocumentBuilder() {
            document = new Document();
        }

        DocumentBuilder add(Field field) {
            document.add(field);
            return this;
        }

        Document build() {
            return document;
        }
    }

}
